/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Haystack Software Inc. All rights reserved.
 *  Licensed under the PolyForm Strict License 1.0.0. See License.txt in the project root for
 *  license information.
 *--------------------------------------------------------------------------------------------*/

/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See code-license.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import "vs/css!./media/editorgroupview"
import {
  EditorGroupModel,
  IEditorOpenOptions,
  IGroupModelChangeEvent,
  ISerializedEditorGroupModel,
  isGroupEditorCloseEvent,
  isGroupEditorOpenEvent,
  isSerializedEditorGroupModel,
} from "vs/workbench/common/editor/editorGroupModel"
import {
  GroupIdentifier,
  CloseDirection,
  IEditorCloseEvent,
  IEditorPane,
  SaveReason,
  IEditorPartOptionsChangeEvent,
  EditorsOrder,
  IVisibleEditorPane,
  EditorResourceAccessor,
  EditorInputCapabilities,
  IUntypedEditorInput,
  DEFAULT_EDITOR_ASSOCIATION,
  SideBySideEditor,
  EditorCloseContext,
  IEditorWillMoveEvent,
  IEditorWillOpenEvent,
  IMatchEditorOptions,
  GroupModelChangeKind,
  IActiveEditorChangeEvent,
  IFindEditorOptions,
  IToolbarActions,
  TEXT_DIFF_EDITOR_ID,
} from "vs/workbench/common/editor"
import {
  ActiveEditorGroupLockedContext,
  ActiveEditorDirtyContext,
  EditorGroupEditorsCountContext,
  ActiveEditorStickyContext,
  ActiveEditorPinnedContext,
  ActiveEditorLastInGroupContext,
  ActiveEditorFirstInGroupContext,
  ResourceContextKey,
  applyAvailableEditorIds,
  ActiveEditorAvailableEditorIdsContext,
  ActiveEditorCanSplitInGroupContext,
  SideBySideEditorActiveContext,
  TextCompareEditorVisibleContext,
  TextCompareEditorActiveContext,
  ActiveEditorContext,
  ActiveEditorReadonlyContext,
  ActiveEditorCanRevertContext,
  ActiveEditorCanToggleReadonlyContext,
  ActiveCompareEditorCanSwapContext,
  MultipleEditorsSelectedInGroupContext,
  TwoEditorsSelectedInGroupContext,
} from "vs/workbench/common/contextkeys"
import { EditorInput } from "vs/workbench/common/editor/editorInput"
import { SideBySideEditorInput } from "vs/workbench/common/editor/sideBySideEditorInput"
import { Emitter, Relay } from "vs/base/common/event"
import { IInstantiationService } from "vs/platform/instantiation/common/instantiation"
import {
  Dimension,
  trackFocus,
  addDisposableListener,
  EventType,
  EventHelper,
  findParentWithClass,
  isAncestor,
  IDomNodePagePosition,
  isMouseEvent,
  getWindow,
  getActiveElement,
} from "vs/base/browser/dom"
import { ServiceCollection } from "vs/platform/instantiation/common/serviceCollection"
import { IContextKeyService } from "vs/platform/contextkey/common/contextkey"
import { ProgressBar } from "vs/base/browser/ui/progressbar/progressbar"
import { IThemeService, Themable } from "vs/platform/theme/common/themeService"
import {
  editorBackground,
  contrastBorder,
} from "vs/platform/theme/common/colorRegistry"
import {
  EDITOR_GROUP_HEADER_TABS_BACKGROUND,
  EDITOR_GROUP_HEADER_NO_TABS_BACKGROUND,
  EDITOR_GROUP_EMPTY_BACKGROUND,
  EDITOR_GROUP_HEADER_BORDER,
} from "vs/workbench/common/theme"
import {
  ICloseEditorsFilter,
  GroupsOrder,
  ICloseEditorOptions,
  ICloseAllEditorsOptions,
  IEditorReplacement,
  IActiveEditorActions,
} from "vs/workbench/services/editor/common/editorGroupsService"
import { EditorPanes } from "vs/workbench/browser/parts/editor/editorPanes"
import { IEditorProgressService } from "vs/platform/progress/common/progress"
import { EditorProgressIndicator } from "vs/workbench/services/progress/browser/progressIndicator"
import { localize } from "vs/nls"
import { coalesce, firstOrDefault } from "vs/base/common/arrays"
import {
  DisposableStore,
  MutableDisposable,
  toDisposable,
} from "vs/base/common/lifecycle"
import { ITelemetryService } from "vs/platform/telemetry/common/telemetry"
import { DeferredPromise, Promises, RunOnceWorker } from "vs/base/common/async"
import {
  EventType as TouchEventType,
  GestureEvent,
} from "vs/base/browser/touch"
import {
  IEditorGroupsView,
  IEditorGroupView,
  fillActiveEditorViewState,
  EditorServiceImpl,
  IEditorGroupTitleHeight,
  IInternalEditorOpenOptions,
  IInternalMoveCopyOptions,
  IInternalEditorCloseOptions,
  IInternalEditorTitleControlOptions,
  IEditorPartsView,
  IEditorGroupViewOptions,
} from "vs/workbench/browser/parts/editor/editor"
import { ActionBar } from "vs/base/browser/ui/actionbar/actionbar"
import { IKeybindingService } from "vs/platform/keybinding/common/keybinding"
import { IAction, SubmenuAction } from "vs/base/common/actions"
import { IMenuService, MenuId } from "vs/platform/actions/common/actions"
import { StandardMouseEvent } from "vs/base/browser/mouseEvent"
import { createAndFillInActionBarActions } from "vs/platform/actions/browser/menuEntryActionViewItem"
import { IContextMenuService } from "vs/platform/contextview/browser/contextView"
import { IEditorService } from "vs/workbench/services/editor/common/editorService"
import { hash } from "vs/base/common/hash"
import { getMimeTypes } from "vs/editor/common/services/languagesAssociations"
import { extname, isEqual } from "vs/base/common/resources"
import { Schemas } from "vs/base/common/network"
import {
  EditorActivation,
  IEditorOptions,
} from "vs/platform/editor/common/editor"
import {
  IFileDialogService,
  ConfirmResult,
  IDialogService,
} from "vs/platform/dialogs/common/dialogs"
import {
  IFilesConfigurationService,
  AutoSaveMode,
} from "vs/workbench/services/filesConfiguration/common/filesConfigurationService"
import { URI } from "vs/base/common/uri"
import { IUriIdentityService } from "vs/platform/uriIdentity/common/uriIdentity"
import {
  isLinux,
  isMacintosh,
  isNative,
  isWindows,
} from "vs/base/common/platform"
import { ILogService } from "vs/platform/log/common/log"
import { TelemetryTrustedValue } from "vs/platform/telemetry/common/telemetryUtils"
import { defaultProgressBarStyles } from "vs/platform/theme/browser/defaultStyles"
import { IBoundarySashes } from "vs/base/browser/ui/sash/sash"
import { EditorGroupWatermark } from "vs/workbench/browser/parts/editor/editorGroupWatermark"
import { EditorTitleControl } from "vs/workbench/browser/parts/editor/editorTitleControl"
import { EditorPane } from "vs/workbench/browser/parts/editor/editorPane"
import { IEditorResolverService } from "vs/workbench/services/editor/common/editorResolverService"
import { IHostService } from "vs/workbench/services/host/browser/host"
import { DiffEditorInput } from "vs/workbench/common/editor/diffEditorInput"
import {
  FileSystemProviderCapabilities,
  IFileService,
} from "vs/platform/files/common/files"
import { IHaystackService } from "vs/workbench/services/haystack/common/haystackService"
import { FileEditorInput } from "vs/workbench/contrib/files/browser/editors/fileEditorInput"
import { isMergeEditorInput } from "vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService"

export class EditorGroupView extends Themable implements IEditorGroupView {
  //#region factory

  static createNew(
    editorPartsView: IEditorPartsView,
    groupsView: IEditorGroupsView,
    groupsLabel: string,
    groupIndex: number,
    instantiationService: IInstantiationService,
    options?: IEditorGroupViewOptions,
  ): IEditorGroupView {
    return instantiationService.createInstance(
      EditorGroupView,
      null,
      editorPartsView,
      groupsView,
      groupsLabel,
      groupIndex,
      options,
    )
  }

  static createFromSerialized(
    serialized: ISerializedEditorGroupModel,
    editorPartsView: IEditorPartsView,
    groupsView: IEditorGroupsView,
    groupsLabel: string,
    groupIndex: number,
    instantiationService: IInstantiationService,
    options?: IEditorGroupViewOptions,
  ): IEditorGroupView {
    return instantiationService.createInstance(
      EditorGroupView,
      serialized,
      editorPartsView,
      groupsView,
      groupsLabel,
      groupIndex,
      options,
    )
  }

  static createCopy(
    copyFrom: IEditorGroupView,
    editorPartsView: IEditorPartsView,
    groupsView: IEditorGroupsView,
    groupsLabel: string,
    groupIndex: number,
    instantiationService: IInstantiationService,
    options?: IEditorGroupViewOptions,
  ): IEditorGroupView {
    return instantiationService.createInstance(
      EditorGroupView,
      copyFrom,
      editorPartsView,
      groupsView,
      groupsLabel,
      groupIndex,
      options,
    )
  }

  //#endregion

  /**
   * Access to the context key service scoped to this editor group.
   */
  readonly scopedContextKeyService: IContextKeyService

  //#region events

  private readonly _onDidFocus = this._register(new Emitter<void>())
  readonly onDidFocus = this._onDidFocus.event

  private readonly _onWillDispose = this._register(new Emitter<void>())
  readonly onWillDispose = this._onWillDispose.event

  private readonly _onDidModelChange = this._register(
    new Emitter<IGroupModelChangeEvent>(),
  )
  readonly onDidModelChange = this._onDidModelChange.event

  private readonly _onDidActiveEditorChange = this._register(
    new Emitter<IActiveEditorChangeEvent>(),
  )
  readonly onDidActiveEditorChange = this._onDidActiveEditorChange.event

  private readonly _onDidOpenEditorFail = this._register(
    new Emitter<EditorInput>(),
  )
  readonly onDidOpenEditorFail = this._onDidOpenEditorFail.event

  private readonly _onWillCloseEditor = this._register(
    new Emitter<IEditorCloseEvent>(),
  )
  readonly onWillCloseEditor = this._onWillCloseEditor.event

  private readonly _onDidCloseEditor = this._register(
    new Emitter<IEditorCloseEvent>(),
  )
  readonly onDidCloseEditor = this._onDidCloseEditor.event

  private readonly _onWillMoveEditor = this._register(
    new Emitter<IEditorWillMoveEvent>(),
  )
  readonly onWillMoveEditor = this._onWillMoveEditor.event

  private readonly _onWillOpenEditor = this._register(
    new Emitter<IEditorWillOpenEvent>(),
  )
  readonly onWillOpenEditor = this._onWillOpenEditor.event

  //#endregion

  private readonly model: EditorGroupModel

  private active: boolean | undefined
  private lastLayout: IDomNodePagePosition | undefined

  private readonly scopedInstantiationService: IInstantiationService

  private readonly resourceContext: ResourceContextKey

  private readonly titleContainer: HTMLElement
  private readonly titleControl: EditorTitleControl

  private readonly progressBar: ProgressBar

  private readonly editorContainer: HTMLElement
  private readonly editorPane: EditorPanes

  private readonly disposedEditorsWorker = this._register(
    new RunOnceWorker<EditorInput>(
      (editors) => this.handleDisposedEditors(editors),
      0,
    ),
  )

  private readonly mapEditorToPendingConfirmation = new Map<
    EditorInput,
    Promise<boolean>
  >()

  private readonly containerToolBarMenuDisposable = this._register(
    new MutableDisposable(),
  )

  private readonly whenRestoredPromise = new DeferredPromise<void>()
  readonly whenRestored = this.whenRestoredPromise.p

  constructor(
    from: IEditorGroupView | ISerializedEditorGroupModel | null,
    private readonly editorPartsView: IEditorPartsView,
    readonly groupsView: IEditorGroupsView,
    private groupsLabel: string,
    private _index: number,
    options: IEditorGroupViewOptions | undefined,
    @IInstantiationService
    private readonly instantiationService: IInstantiationService,
    @IContextKeyService private readonly contextKeyService: IContextKeyService,
    @IThemeService themeService: IThemeService,
    @ITelemetryService private readonly telemetryService: ITelemetryService,
    @IKeybindingService private readonly keybindingService: IKeybindingService,
    @IMenuService private readonly menuService: IMenuService,
    @IContextMenuService
    private readonly contextMenuService: IContextMenuService,
    @IFileDialogService private readonly fileDialogService: IFileDialogService,
    @IEditorService private readonly editorService: EditorServiceImpl,
    @IFilesConfigurationService
    private readonly filesConfigurationService: IFilesConfigurationService,
    @IUriIdentityService
    private readonly uriIdentityService: IUriIdentityService,
    @ILogService private readonly logService: ILogService,
    @IEditorResolverService
    private readonly editorResolverService: IEditorResolverService,
    @IHostService private readonly hostService: IHostService,
    @IDialogService private readonly dialogService: IDialogService,
    @IFileService private readonly fileService: IFileService,
    @IHaystackService private readonly _haystackService: IHaystackService,
  ) {
    super(themeService)

    if (from instanceof EditorGroupView) {
      this.model = this._register(from.model.clone())
    } else if (isSerializedEditorGroupModel(from)) {
      this.model = this._register(
        instantiationService.createInstance(EditorGroupModel, from),
      )
    } else {
      this.model = this._register(
        instantiationService.createInstance(EditorGroupModel, undefined),
      )
    }

    //#region create()
    {
      // Scoped context key service
      this.scopedContextKeyService = this._register(
        this.contextKeyService.createScoped(this.element),
      )

      // Container
      this.element.classList.add(
        ...coalesce([
          "editor-group-container",
          this.model.isLocked ? "locked" : undefined,
        ]),
      )

      // Container listeners
      this.registerContainerListeners()

      // Container toolbar
      this.createContainerToolbar()

      // Container context menu
      this.createContainerContextMenu()

      // Watermark & shortcuts
      this._register(
        this.instantiationService.createInstance(
          EditorGroupWatermark,
          this.element,
        ),
      )

      // Progress bar
      this.progressBar = this._register(
        new ProgressBar(this.element, defaultProgressBarStyles),
      )
      this.progressBar.hide()

      // Scoped instantiation service
      this.scopedInstantiationService = this._register(
        this.instantiationService.createChild(
          new ServiceCollection(
            [IContextKeyService, this.scopedContextKeyService],
            [
              IEditorProgressService,
              this._register(
                new EditorProgressIndicator(this.progressBar, this),
              ),
            ],
          ),
        ),
      )

      // Context keys
      this.resourceContext = this._register(
        this.scopedInstantiationService.createInstance(ResourceContextKey),
      )
      this.handleGroupContextKeys()

      // Title container
      this.titleContainer = document.createElement("div")
      this.titleContainer.classList.add("title")
      this.element.appendChild(this.titleContainer)

      // Title control
      this.titleControl = this._register(
        this.scopedInstantiationService.createInstance(
          EditorTitleControl,
          this.titleContainer,
          this.editorPartsView,
          this.groupsView,
          this,
          this.model,
        ),
      )

      // Editor container
      this.editorContainer = document.createElement("div")
      this.editorContainer.classList.add("editor-container")
      this.element.appendChild(this.editorContainer)

      // Editor pane
      this.editorPane = this._register(
        this.scopedInstantiationService.createInstance(
          EditorPanes,
          this.element,
          this.editorContainer,
          this,
        ),
      )
      this._onDidChange.input = this.editorPane.onDidChangeSizeConstraints

      // Track Focus
      this.doTrackFocus()

      // Update containers
      this.updateTitleContainer()
      this.updateContainer()

      // Update styles
      this.updateStyles()
    }
    //#endregion

    // // Restore editors if provided
    const restoreEditorsPromise =
      this.restoreEditors(from, options) ?? Promise.resolve()

    // Signal restored once editors have restored
    restoreEditorsPromise.finally(() => {
      this.whenRestoredPromise.complete()
    })

    // Register Listeners
    this.registerListeners()
  }

  private handleGroupContextKeys(): void {
    const groupActiveEditorDirtyContext = this.editorPartsView.bind(
      ActiveEditorDirtyContext,
      this,
    )
    const groupActiveEditorPinnedContext = this.editorPartsView.bind(
      ActiveEditorPinnedContext,
      this,
    )
    const groupActiveEditorFirstContext = this.editorPartsView.bind(
      ActiveEditorFirstInGroupContext,
      this,
    )
    const groupActiveEditorLastContext = this.editorPartsView.bind(
      ActiveEditorLastInGroupContext,
      this,
    )
    const groupActiveEditorStickyContext = this.editorPartsView.bind(
      ActiveEditorStickyContext,
      this,
    )
    const groupEditorsCountContext = this.editorPartsView.bind(
      EditorGroupEditorsCountContext,
      this,
    )
    const groupLockedContext = this.editorPartsView.bind(
      ActiveEditorGroupLockedContext,
      this,
    )

    const multipleEditorsSelectedContext =
      MultipleEditorsSelectedInGroupContext.bindTo(this.scopedContextKeyService)
    const twoEditorsSelectedContext = TwoEditorsSelectedInGroupContext.bindTo(
      this.scopedContextKeyService,
    )

    const groupActiveEditorContext = this.editorPartsView.bind(
      ActiveEditorContext,
      this,
    )
    const groupActiveEditorIsReadonly = this.editorPartsView.bind(
      ActiveEditorReadonlyContext,
      this,
    )
    const groupActiveEditorCanRevert = this.editorPartsView.bind(
      ActiveEditorCanRevertContext,
      this,
    )
    const groupActiveEditorCanToggleReadonly = this.editorPartsView.bind(
      ActiveEditorCanToggleReadonlyContext,
      this,
    )
    const groupActiveCompareEditorCanSwap = this.editorPartsView.bind(
      ActiveCompareEditorCanSwapContext,
      this,
    )
    const groupTextCompareEditorVisibleContext = this.editorPartsView.bind(
      TextCompareEditorVisibleContext,
      this,
    )
    const groupTextCompareEditorActiveContext = this.editorPartsView.bind(
      TextCompareEditorActiveContext,
      this,
    )

    const groupActiveEditorAvailableEditorIds = this.editorPartsView.bind(
      ActiveEditorAvailableEditorIdsContext,
      this,
    )
    const groupActiveEditorCanSplitInGroupContext = this.editorPartsView.bind(
      ActiveEditorCanSplitInGroupContext,
      this,
    )
    const groupActiveEditorIsSideBySideEditorContext =
      this.editorPartsView.bind(SideBySideEditorActiveContext, this)

    const activeEditorListener = this._register(new MutableDisposable())

    const observeActiveEditor = () => {
      activeEditorListener.clear()

      this.scopedContextKeyService.bufferChangeEvents(() => {
        const activeEditor = this.activeEditor
        const activeEditorPane = this.activeEditorPane

        this.resourceContext.set(
          EditorResourceAccessor.getOriginalUri(activeEditor, {
            supportSideBySide: SideBySideEditor.PRIMARY,
          }),
        )

        applyAvailableEditorIds(
          groupActiveEditorAvailableEditorIds,
          activeEditor,
          this.editorResolverService,
        )

        if (activeEditor) {
          groupActiveEditorCanSplitInGroupContext.set(
            activeEditor.hasCapability(EditorInputCapabilities.CanSplitInGroup),
          )
          groupActiveEditorIsSideBySideEditorContext.set(
            activeEditor.typeId === SideBySideEditorInput.ID,
          )

          groupActiveEditorDirtyContext.set(
            activeEditor.isDirty() && !activeEditor.isSaving(),
          )
          activeEditorListener.value = activeEditor.onDidChangeDirty(() => {
            groupActiveEditorDirtyContext.set(
              activeEditor.isDirty() && !activeEditor.isSaving(),
            )
          })
        } else {
          groupActiveEditorCanSplitInGroupContext.set(false)
          groupActiveEditorIsSideBySideEditorContext.set(false)
          groupActiveEditorDirtyContext.set(false)
        }

        if (activeEditorPane) {
          groupActiveEditorContext.set(activeEditorPane.getId())
          groupActiveEditorCanRevert.set(
            !activeEditorPane.input.hasCapability(
              EditorInputCapabilities.Untitled,
            ),
          )
          groupActiveEditorIsReadonly.set(!!activeEditorPane.input.isReadonly())

          const primaryEditorResource = EditorResourceAccessor.getOriginalUri(
            activeEditorPane.input,
            { supportSideBySide: SideBySideEditor.PRIMARY },
          )
          const secondaryEditorResource = EditorResourceAccessor.getOriginalUri(
            activeEditorPane.input,
            { supportSideBySide: SideBySideEditor.SECONDARY },
          )
          groupActiveCompareEditorCanSwap.set(
            activeEditorPane.input instanceof DiffEditorInput &&
              !activeEditorPane.input.original.isReadonly() &&
              !!primaryEditorResource &&
              (this.fileService.hasProvider(primaryEditorResource) ||
                primaryEditorResource.scheme === Schemas.untitled) &&
              !!secondaryEditorResource &&
              (this.fileService.hasProvider(secondaryEditorResource) ||
                secondaryEditorResource.scheme === Schemas.untitled),
          )
          groupActiveEditorCanToggleReadonly.set(
            !!primaryEditorResource &&
              this.fileService.hasProvider(primaryEditorResource) &&
              !this.fileService.hasCapability(
                primaryEditorResource,
                FileSystemProviderCapabilities.Readonly,
              ),
          )

          const activePaneDiffEditor =
            activeEditorPane?.getId() === TEXT_DIFF_EDITOR_ID
          groupTextCompareEditorActiveContext.set(activePaneDiffEditor)
          groupTextCompareEditorVisibleContext.set(activePaneDiffEditor)
        } else {
          groupActiveEditorContext.reset()
          groupActiveEditorCanRevert.reset()
          groupActiveEditorIsReadonly.reset()
          groupActiveCompareEditorCanSwap.reset()
          groupActiveEditorCanToggleReadonly.reset()
        }
      })
    }

    // Update group contexts based on group changes
    const updateGroupContextKeys = (e: IGroupModelChangeEvent) => {
      switch (e.kind) {
        case GroupModelChangeKind.GROUP_LOCKED:
          groupLockedContext.set(this.isLocked)
          break
        case GroupModelChangeKind.EDITOR_ACTIVE:
          groupActiveEditorFirstContext.set(
            this.model.isFirst(this.model.activeEditor),
          )
          groupActiveEditorLastContext.set(
            this.model.isLast(this.model.activeEditor),
          )
          groupActiveEditorPinnedContext.set(
            this.model.activeEditor
              ? this.model.isPinned(this.model.activeEditor)
              : false,
          )
          groupActiveEditorStickyContext.set(
            this.model.activeEditor
              ? this.model.isSticky(this.model.activeEditor)
              : false,
          )
          break
        case GroupModelChangeKind.EDITOR_CLOSE:
          groupActiveEditorPinnedContext.set(
            this.model.activeEditor
              ? this.model.isPinned(this.model.activeEditor)
              : false,
          )
          groupActiveEditorStickyContext.set(
            this.model.activeEditor
              ? this.model.isSticky(this.model.activeEditor)
              : false,
          )
        case GroupModelChangeKind.EDITOR_OPEN:
        case GroupModelChangeKind.EDITOR_MOVE:
          groupActiveEditorFirstContext.set(
            this.model.isFirst(this.model.activeEditor),
          )
          groupActiveEditorLastContext.set(
            this.model.isLast(this.model.activeEditor),
          )
          break
        case GroupModelChangeKind.EDITOR_PIN:
          if (e.editor && e.editor === this.model.activeEditor) {
            groupActiveEditorPinnedContext.set(
              this.model.isPinned(this.model.activeEditor),
            )
          }
          break
        case GroupModelChangeKind.EDITOR_STICKY:
          if (e.editor && e.editor === this.model.activeEditor) {
            groupActiveEditorStickyContext.set(
              this.model.isSticky(this.model.activeEditor),
            )
          }
          break
        case GroupModelChangeKind.EDITORS_SELECTION:
          multipleEditorsSelectedContext.set(
            this.model.selectedEditors.length > 1,
          )
          twoEditorsSelectedContext.set(this.model.selectedEditors.length === 2)
          break
      }

      // Group editors count context
      groupEditorsCountContext.set(this.count)
    }

    this._register(this.onDidModelChange((e) => updateGroupContextKeys(e)))

    // Track the active editor and update context key that reflects
    // the dirty state of this editor
    this._register(this.onDidActiveEditorChange(() => observeActiveEditor()))

    // Update context keys on startup
    observeActiveEditor()
    updateGroupContextKeys({ kind: GroupModelChangeKind.EDITOR_ACTIVE })
    updateGroupContextKeys({ kind: GroupModelChangeKind.GROUP_LOCKED })
  }

  private registerContainerListeners(): void {
    // Open new file via doubleclick on empty container
    this._register(
      addDisposableListener(this.element, EventType.DBLCLICK, (e) => {
        if (this.isEmpty) {
          EventHelper.stop(e)

          this.editorService.openEditor(
            {
              resource: undefined,
              options: {
                pinned: true,
                override: DEFAULT_EDITOR_ASSOCIATION.id,
              },
            },
            this.id,
          )
        }
      }),
    )

    // Close empty editor group via middle mouse click
    this._register(
      addDisposableListener(this.element, EventType.AUXCLICK, (e) => {
        if (this.isEmpty && e.button === 1 /* Middle Button */) {
          EventHelper.stop(e, true)

          this.groupsView.removeGroup(this)
        }
      }),
    )
  }

  private createContainerToolbar(): void {
    // Toolbar Container
    const toolbarContainer = document.createElement("div")
    toolbarContainer.classList.add("editor-group-container-toolbar")
    this.element.appendChild(toolbarContainer)

    // Toolbar
    const containerToolbar = this._register(
      new ActionBar(toolbarContainer, {
        ariaLabel: localize(
          "ariaLabelGroupActions",
          "Empty editor group actions",
        ),
        highlightToggledItems: true,
      }),
    )

    // Toolbar actions
    const containerToolbarMenu = this._register(
      this.menuService.createMenu(
        MenuId.EmptyEditorGroup,
        this.scopedContextKeyService,
      ),
    )
    const updateContainerToolbar = () => {
      const actions: IToolbarActions = { primary: [], secondary: [] }

      // Clear old actions
      this.containerToolBarMenuDisposable.value = toDisposable(() =>
        containerToolbar.clear(),
      )

      // Create new actions
      createAndFillInActionBarActions(
        containerToolbarMenu,
        { arg: { groupId: this.id }, shouldForwardArgs: true },
        actions,
        "navigation",
      )

      for (const action of [...actions.primary, ...actions.secondary]) {
        const keybinding = this.keybindingService.lookupKeybinding(action.id)
        containerToolbar.push(action, {
          icon: true,
          label: false,
          keybinding: keybinding?.getLabel(),
        })
      }
    }
    updateContainerToolbar()
    this._register(containerToolbarMenu.onDidChange(updateContainerToolbar))
  }

  private createContainerContextMenu(): void {
    this._register(
      addDisposableListener(this.element, EventType.CONTEXT_MENU, (e) =>
        this.onShowContainerContextMenu(e),
      ),
    )
    this._register(
      addDisposableListener(this.element, TouchEventType.Contextmenu, () =>
        this.onShowContainerContextMenu(),
      ),
    )
  }

  private onShowContainerContextMenu(e?: MouseEvent): void {
    if (!this.isEmpty) {
      return // only for empty editor groups
    }

    // Find target anchor
    let anchor: HTMLElement | StandardMouseEvent = this.element
    if (e) {
      anchor = new StandardMouseEvent(getWindow(this.element), e)
    }

    // Show it
    this.contextMenuService.showContextMenu({
      menuId: MenuId.EmptyEditorGroupContext,
      contextKeyService: this.contextKeyService,
      getAnchor: () => anchor,
      onHide: () => {
        this.focus()
      },
    })
  }

  private doTrackFocus(): void {
    // Container
    const containerFocusTracker = this._register(trackFocus(this.element))
    this._register(
      containerFocusTracker.onDidFocus(() => {
        if (this.isEmpty) {
          this._onDidFocus.fire() // only when empty to prevent duplicate events from `editorPane.onDidFocus`
        }
      }),
    )

    // Title Container
    const handleTitleClickOrTouch = (e: MouseEvent | GestureEvent): void => {
      let target: HTMLElement
      if (isMouseEvent(e)) {
        if (
          e.button !== 0 /* middle/right mouse button */ ||
          (isMacintosh && e.ctrlKey) /* macOS context menu */
        ) {
          return undefined
        }

        target = e.target as HTMLElement
      } else {
        target = (e as GestureEvent).initialTarget as HTMLElement
      }

      if (
        findParentWithClass(target, "monaco-action-bar", this.titleContainer) ||
        findParentWithClass(
          target,
          "monaco-breadcrumb-item",
          this.titleContainer,
        )
      ) {
        return // not when clicking on actions or breadcrumbs
      }

      // timeout to keep focus in editor after mouse up
      setTimeout(() => {
        this.focus()
      })
    }

    this._register(
      addDisposableListener(this.titleContainer, EventType.MOUSE_DOWN, (e) =>
        handleTitleClickOrTouch(e),
      ),
    )
    this._register(
      addDisposableListener(this.titleContainer, TouchEventType.Tap, (e) =>
        handleTitleClickOrTouch(e),
      ),
    )

    // Editor pane
    this._register(
      this.editorPane.onDidFocus(() => {
        this._onDidFocus.fire()
      }),
    )
  }

  private updateContainer(): void {
    // Empty Container: add some empty container attributes
    if (this.isEmpty) {
      this.element.classList.add("empty")
      this.element.tabIndex = 0
      this.element.setAttribute(
        "aria-label",
        localize("emptyEditorGroup", "{0} (empty)", this.ariaLabel),
      )
    }

    // Non-Empty Container: revert empty container attributes
    else {
      this.element.classList.remove("empty")
      this.element.removeAttribute("tabIndex")
      this.element.removeAttribute("aria-label")
    }

    // Update styles
    this.updateStyles()
  }

  private updateTitleContainer(): void {
    this.titleContainer.classList.toggle(
      "tabs",
      this.groupsView.partOptions.showTabs === "multiple",
    )
    this.titleContainer.classList.toggle(
      "show-file-icons",
      this.groupsView.partOptions.showIcons,
    )
  }

  private restoreEditors(
    from: IEditorGroupView | ISerializedEditorGroupModel | null,
    groupViewOptions?: IEditorGroupViewOptions,
  ): Promise<void> | undefined {
    if (this.count === 0) {
      return // nothing to show
    }

    // Determine editor options
    let options: IEditorOptions
    if (from instanceof EditorGroupView) {
      options = fillActiveEditorViewState(from) // if we copy from another group, ensure to copy its active editor viewstate
    } else {
      options = Object.create(null)
    }

    const activeEditor = this.model.activeEditor
    if (!activeEditor || activeEditor.resource == null) {
      return
    }

    options.pinned = this.model.isPinned(activeEditor) // preserve pinned state
    options.sticky = this.model.isSticky(activeEditor) // preserve sticky state
    options.preserveFocus = true // handle focus after editor is restored

    // Show active editor (intentionally not using async to keep
    // `restoreEditors` from executing in same stack)

    if (isMergeEditorInput(activeEditor)) {
      return this._haystackService
        .createMergeEditor(
          {
            base: { resource: activeEditor.base },
            input1: {
              description: "",
              resource: activeEditor.input1.uri,
            },
            input2: {
              description: "",
              resource: activeEditor.input1.uri,
            },
            result: { resource: activeEditor.resource },
          },
          { forceNewEditor: true },
        )
        .then(() => {})
    }

    if (activeEditor instanceof FileEditorInput) {
      const fileEditor = activeEditor as FileEditorInput
      const editRange = fileEditor.editRange

      if (editRange != null) {
        const uri = activeEditor.resource

        return this._haystackService
          .getDeepestSymbolContainingRange({
            uri,
            range: editRange,
          })
          .then(async (symbol) => {
            if (symbol != null) {
              await this._haystackService.createSymbolEditorWithSymbol(
                symbol.name,
                symbol.kind,
                uri,
                symbol.range,
                {
                  existingEditorInput: {
                    input: activeEditor,
                    groupId: this.id,
                  },
                  doNotPanTo: true,
                  forceNewEditor: true,
                  selectionRange: {
                    startLineNumber: symbol.selectionRange.startLineNumber,
                    startColumn: symbol.selectionRange.startColumn,
                    endLineNumber: symbol.selectionRange.startLineNumber,
                    endColumn: symbol.selectionRange.startColumn,
                  },
                },
                options,
              )

              return
            }

            await this._haystackService.createFileEditor(
              uri,
              {
                selectionRange: {
                  startLineNumber: editRange.startLineNumber,
                  startColumn: editRange.startColumn,
                  endLineNumber: editRange.startLineNumber,
                  endColumn: editRange.startColumn,
                },
                existingEditorInput: {
                  input: activeEditor,
                  groupId: this.id,
                },
                doNotPanTo: true,
                forceNewEditor: true,
              },
              options,
            )
          })
      }
    }

    if (
      activeEditor.resource.scheme !== "file" &&
      activeEditor.resource.scheme !== "vscode-remote"
    ) {
      return this._haystackService
        .createModalEditor(
          activeEditor,
          {
            existingEditorInput: { input: activeEditor, groupId: this.id },
            doNotPanTo: true,
            forceNewEditor: true,
          },
          options,
        )
        .then(() => {})
    }

    return this._haystackService
      .createFileEditor(
        activeEditor.resource,
        {
          existingEditorInput: { input: activeEditor, groupId: this.id },
          forceNewEditor: true,
          doNotPanTo: true,
        },
        options,
      )
      .then(() => {})
  }

  //#region event handling

  private registerListeners(): void {
    // Model Events
    this._register(
      this.model.onDidModelChange((e) => this.onDidGroupModelChange(e)),
    )

    // Option Changes
    this._register(
      this.groupsView.onDidChangeEditorPartOptions((e) =>
        this.onDidChangeEditorPartOptions(e),
      ),
    )

    // Visibility
    this._register(
      this.groupsView.onDidVisibilityChange((e) =>
        this.onDidVisibilityChange(e),
      ),
    )

    // Focus
    this._register(this.onDidFocus(() => this.onDidGainFocus()))
  }

  private onDidGroupModelChange(e: IGroupModelChangeEvent): void {
    // Re-emit to outside
    this._onDidModelChange.fire(e)

    // Handle within

    switch (e.kind) {
      case GroupModelChangeKind.GROUP_LOCKED:
        this.element.classList.toggle("locked", this.isLocked)
        break
      case GroupModelChangeKind.EDITORS_SELECTION:
        this.onDidChangeEditorSelection()
        break
    }

    if (!e.editor) {
      return
    }

    switch (e.kind) {
      case GroupModelChangeKind.EDITOR_OPEN:
        if (isGroupEditorOpenEvent(e)) {
          this.onDidOpenEditor(e.editor, e.editorIndex)
        }
        break
      case GroupModelChangeKind.EDITOR_CLOSE:
        if (isGroupEditorCloseEvent(e)) {
          this.handleOnDidCloseEditor(
            e.editor,
            e.editorIndex,
            e.context,
            e.sticky,
          )
        }
        break
      case GroupModelChangeKind.EDITOR_WILL_DISPOSE:
        this.onWillDisposeEditor(e.editor)
        break
      case GroupModelChangeKind.EDITOR_DIRTY:
        this.onDidChangeEditorDirty(e.editor)
        break
      case GroupModelChangeKind.EDITOR_TRANSIENT:
        this.onDidChangeEditorTransient(e.editor)
        break
      case GroupModelChangeKind.EDITOR_LABEL:
        this.onDidChangeEditorLabel(e.editor)
        break
    }
  }

  private onDidOpenEditor(editor: EditorInput, editorIndex: number): void {
    /* __GDPR__
      "editorOpened" : {
        "owner": "bpasero",
        "${include}": [
          "${EditorTelemetryDescriptor}"
        ]
      }
    */
    this.telemetryService.publicLog(
      "editorOpened",
      this.toEditorTelemetryDescriptor(editor),
    )

    // Update container
    this.updateContainer()
  }

  private handleOnDidCloseEditor(
    editor: EditorInput,
    editorIndex: number,
    context: EditorCloseContext,
    sticky: boolean,
  ): void {
    // Before close
    this._onWillCloseEditor.fire({
      groupId: this.id,
      editor,
      context,
      index: editorIndex,
      sticky,
    })

    // Handle event
    const editorsToClose: EditorInput[] = [editor]

    // Include both sides of side by side editors when being closed
    if (editor instanceof SideBySideEditorInput) {
      editorsToClose.push(editor.primary, editor.secondary)
    }

    // For each editor to close, we call dispose() to free up any resources.
    // However, certain editors might be shared across multiple editor groups
    // (including being visible in side by side / diff editors) and as such we
    // only dispose when they are not opened elsewhere.
    for (const editor of editorsToClose) {
      if (this.canDispose(editor)) {
        editor.dispose()
      }
    }

    /* __GDPR__
      "editorClosed" : {
        "owner": "bpasero",
        "${include}": [
          "${EditorTelemetryDescriptor}"
        ]
      }
    */
    this.telemetryService.publicLog(
      "editorClosed",
      this.toEditorTelemetryDescriptor(editor),
    )

    // Update container
    this.updateContainer()

    // Event
    this._onDidCloseEditor.fire({
      groupId: this.id,
      editor,
      context,
      index: editorIndex,
      sticky,
    })
  }

  private canDispose(editor: EditorInput): boolean {
    for (const groupView of this.editorPartsView.groups) {
      if (
        groupView instanceof EditorGroupView &&
        groupView.model.contains(editor, {
          strictEquals: true, // only if this input is not shared across editor groups
          supportSideBySide: SideBySideEditor.ANY, // include any side of an opened side by side editor
        })
      ) {
        return false
      }
    }

    return true
  }

  private toResourceTelemetryDescriptor(resource: URI): object | undefined {
    if (!resource) {
      return undefined
    }

    const path = resource
      ? resource.scheme === Schemas.file
        ? resource.fsPath
        : resource.path
      : undefined
    if (!path) {
      return undefined
    }

    // Remove query parameters from the resource extension
    let resourceExt = extname(resource)
    const queryStringLocation = resourceExt.indexOf("?")
    resourceExt =
      queryStringLocation !== -1
        ? resourceExt.substr(0, queryStringLocation)
        : resourceExt

    return {
      mimeType: new TelemetryTrustedValue(getMimeTypes(resource).join(", ")),
      scheme: resource.scheme,
      ext: resourceExt,
      path: hash(path),
    }
  }

  private toEditorTelemetryDescriptor(editor: EditorInput): object {
    const descriptor = editor.getTelemetryDescriptor()

    const resource = EditorResourceAccessor.getOriginalUri(editor, {
      supportSideBySide: SideBySideEditor.BOTH,
    })
    if (URI.isUri(resource)) {
      descriptor["resource"] = this.toResourceTelemetryDescriptor(resource)

      /* __GDPR__FRAGMENT__
        "EditorTelemetryDescriptor" : {
          "resource": { "${inline}": [ "${URIDescriptor}" ] }
        }
      */
      return descriptor
    } else if (resource) {
      if (resource.primary) {
        descriptor["resource"] = this.toResourceTelemetryDescriptor(
          resource.primary,
        )
      }
      if (resource.secondary) {
        descriptor["resourceSecondary"] = this.toResourceTelemetryDescriptor(
          resource.secondary,
        )
      }
      /* __GDPR__FRAGMENT__
        "EditorTelemetryDescriptor" : {
          "resource": { "${inline}": [ "${URIDescriptor}" ] },
          "resourceSecondary": { "${inline}": [ "${URIDescriptor}" ] }
        }
      */
      return descriptor
    }

    return descriptor
  }

  private onWillDisposeEditor(editor: EditorInput): void {
    // To prevent race conditions, we handle disposed editors in our worker with a timeout
    // because it can happen that an input is being disposed with the intent to replace
    // it with some other input right after.
    this.disposedEditorsWorker.work(editor)
  }

  private handleDisposedEditors(disposedEditors: EditorInput[]): void {
    // Split between visible and hidden editors
    let activeEditor: EditorInput | undefined
    const inactiveEditors: EditorInput[] = []
    for (const disposedEditor of disposedEditors) {
      const editorFindResult = this.model.findEditor(disposedEditor)
      if (!editorFindResult) {
        continue // not part of the model anymore
      }

      const editor = editorFindResult[0]
      if (!editor.isDisposed()) {
        continue // editor got reopened meanwhile
      }

      if (this.model.isActive(editor)) {
        activeEditor = editor
      } else {
        inactiveEditors.push(editor)
      }
    }

    // Close all inactive editors first to prevent UI flicker
    for (const inactiveEditor of inactiveEditors) {
      this.doCloseEditor(inactiveEditor, true)
    }

    // Close active one last
    if (activeEditor) {
      this.doCloseEditor(activeEditor, true)
    }
  }

  private onDidChangeEditorPartOptions(
    event: IEditorPartOptionsChangeEvent,
  ): void {
    // Title container
    this.updateTitleContainer()

    // Title control
    this.titleControl.updateOptions(event.oldPartOptions, event.newPartOptions)

    // Title control switch between singleEditorTabs, multiEditorTabs and multiRowEditorTabs
    if (
      event.oldPartOptions.showTabs !== event.newPartOptions.showTabs ||
      event.oldPartOptions.tabHeight !== event.newPartOptions.tabHeight ||
      (event.oldPartOptions.showTabs === "multiple" &&
        event.oldPartOptions.pinnedTabsOnSeparateRow !==
          event.newPartOptions.pinnedTabsOnSeparateRow)
    ) {
      // Re-layout
      this.relayout()

      // Ensure to show active editor if any
      if (this.model.activeEditor) {
        this.titleControl.openEditor(this.model.activeEditor)
      }
    }

    // Styles
    this.updateStyles()

    // Pin preview editor once user disables preview
    if (
      event.oldPartOptions.enablePreview &&
      !event.newPartOptions.enablePreview
    ) {
      if (this.model.previewEditor) {
        this.pinEditor(this.model.previewEditor)
      }
    }
  }

  private onDidChangeEditorDirty(editor: EditorInput): void {
    // Always show dirty editors pinned
    this.pinEditor(editor)

    // Forward to title control
    this.titleControl.updateEditorDirty(editor)
  }

  private onDidChangeEditorTransient(editor: EditorInput): void {
    const transient = this.model.isTransient(editor)

    // Transient state overrides the `enablePreview` setting,
    // so when an editor leaves the transient state, we have
    // to ensure its preview state is also cleared.
    if (!transient && !this.groupsView.partOptions.enablePreview) {
      this.pinEditor(editor)
    }
  }

  private onDidChangeEditorLabel(editor: EditorInput): void {
    // Forward to title control
    this.titleControl.updateEditorLabel(editor)
  }

  private onDidChangeEditorSelection(): void {
    // Forward to title control
    this.titleControl.updateEditorSelections()
  }

  private onDidVisibilityChange(visible: boolean): void {
    // Forward to active editor pane
    this.editorPane.setVisible(visible)
  }

  private onDidGainFocus(): void {
    if (this.activeEditor) {
      // We aggressively clear the transient state of editors
      // as soon as the group gains focus. This is to ensure
      // that the transient state is not staying around when
      // the user interacts with the editor.

      this.model.setTransient(this.activeEditor, false)
    }
  }

  //#endregion

  //#region IEditorGroupView

  get index(): number {
    return this._index
  }

  get label(): string {
    if (this.groupsLabel) {
      return localize(
        "groupLabelLong",
        "{0}: Group {1}",
        this.groupsLabel,
        this._index + 1,
      )
    }

    return localize("groupLabel", "Group {0}", this._index + 1)
  }

  get ariaLabel(): string {
    if (this.groupsLabel) {
      return localize(
        "groupAriaLabelLong",
        "{0}: Editor Group {1}",
        this.groupsLabel,
        this._index + 1,
      )
    }

    return localize("groupAriaLabel", "Editor Group {0}", this._index + 1)
  }

  private _disposed = false
  get disposed(): boolean {
    return this._disposed
  }

  get isEmpty(): boolean {
    return this.count === 0
  }

  get titleHeight(): IEditorGroupTitleHeight {
    return this.titleControl.getHeight()
  }

  notifyIndexChanged(newIndex: number): void {
    if (this._index !== newIndex) {
      this._index = newIndex
      this.model.setIndex(newIndex)
    }
  }

  notifyLabelChanged(newLabel: string): void {
    if (this.groupsLabel !== newLabel) {
      this.groupsLabel = newLabel
      this.model.setLabel(newLabel)
    }
  }

  setActive(isActive: boolean): void {
    this.active = isActive

    // Clear selection when group no longer active
    if (!isActive && this.activeEditor && this.selectedEditors.length > 1) {
      this.setSelection(this.activeEditor, [])
    }

    // Update container
    this.element.classList.toggle("active", isActive)
    this.element.classList.toggle("inactive", !isActive)

    // Update title control
    this.titleControl.setActive(isActive)

    // Update styles
    this.updateStyles()

    // Update model
    this.model.setActive(undefined /* entire group got active */)
  }

  //#endregion

  //#region basics()

  get id(): GroupIdentifier {
    return this.model.id
  }

  get windowId(): number {
    return this.groupsView.windowId
  }

  get editors(): EditorInput[] {
    return this.model.getEditors(EditorsOrder.SEQUENTIAL)
  }

  get count(): number {
    return this.model.count
  }

  get stickyCount(): number {
    return this.model.stickyCount
  }

  get activeEditorPane(): IVisibleEditorPane | undefined {
    return this.editorPane
      ? (this.editorPane.activeEditorPane ?? undefined)
      : undefined
  }

  get activeEditor(): EditorInput | null {
    return this.model.activeEditor
  }

  get selectedEditors(): EditorInput[] {
    return this.model.selectedEditors
  }

  get previewEditor(): EditorInput | null {
    return this.model.previewEditor
  }

  isPinned(editorOrIndex: EditorInput | number): boolean {
    return this.model.isPinned(editorOrIndex)
  }

  isSticky(editorOrIndex: EditorInput | number): boolean {
    return this.model.isSticky(editorOrIndex)
  }

  isSelected(editor: EditorInput): boolean {
    return this.model.isSelected(editor)
  }

  isTransient(editorOrIndex: EditorInput | number): boolean {
    return this.model.isTransient(editorOrIndex)
  }

  isActive(editor: EditorInput | IUntypedEditorInput): boolean {
    return this.model.isActive(editor)
  }

  async setSelection(
    activeSelectedEditor: EditorInput,
    inactiveSelectedEditors: EditorInput[],
  ): Promise<void> {
    if (!this.isActive(activeSelectedEditor)) {
      // The active selected editor is not yet opened, so we go
      // through `openEditor` to show it. We pass the inactive
      // selection as internal options
      await this.openEditor(
        activeSelectedEditor,
        { activation: EditorActivation.ACTIVATE },
        { inactiveSelection: inactiveSelectedEditors },
      )
    } else {
      this.model.setSelection(activeSelectedEditor, inactiveSelectedEditors)
    }
  }

  contains(
    candidate: EditorInput | IUntypedEditorInput,
    options?: IMatchEditorOptions,
  ): boolean {
    return this.model.contains(candidate, options)
  }

  getEditors(
    order: EditorsOrder,
    options?: { excludeSticky?: boolean },
  ): EditorInput[] {
    return this.model.getEditors(order, options)
  }

  findEditors(resource: URI, options?: IFindEditorOptions): EditorInput[] {
    const canonicalResource = this.uriIdentityService.asCanonicalUri(resource)
    return this.getEditors(EditorsOrder.SEQUENTIAL).filter((editor) => {
      if (editor.resource && isEqual(editor.resource, canonicalResource)) {
        return true
      }

      // Support side by side editor primary side if specified
      if (
        options?.supportSideBySide === SideBySideEditor.PRIMARY ||
        options?.supportSideBySide === SideBySideEditor.ANY
      ) {
        const primaryResource = EditorResourceAccessor.getCanonicalUri(editor, {
          supportSideBySide: SideBySideEditor.PRIMARY,
        })
        if (primaryResource && isEqual(primaryResource, canonicalResource)) {
          return true
        }
      }

      // Support side by side editor secondary side if specified
      if (
        options?.supportSideBySide === SideBySideEditor.SECONDARY ||
        options?.supportSideBySide === SideBySideEditor.ANY
      ) {
        const secondaryResource = EditorResourceAccessor.getCanonicalUri(
          editor,
          { supportSideBySide: SideBySideEditor.SECONDARY },
        )
        if (
          secondaryResource &&
          isEqual(secondaryResource, canonicalResource)
        ) {
          return true
        }
      }

      return false
    })
  }

  getEditorByIndex(index: number): EditorInput | undefined {
    return this.model.getEditorByIndex(index)
  }

  getIndexOfEditor(editor: EditorInput): number {
    return this.model.indexOf(editor)
  }

  isFirst(editor: EditorInput): boolean {
    return this.model.isFirst(editor)
  }

  isLast(editor: EditorInput): boolean {
    return this.model.isLast(editor)
  }

  focus(): void {
    // Pass focus to editor panes
    if (this.activeEditorPane) {
      this.activeEditorPane.focus()
    } else {
      this.element.focus()
    }

    // Event
    this._onDidFocus.fire()
  }

  pinEditor(
    candidate: EditorInput | undefined = this.activeEditor || undefined,
  ): void {
    if (candidate && !this.model.isPinned(candidate)) {
      // Update model
      const editor = this.model.pin(candidate)

      // Forward to title control
      if (editor) {
        this.titleControl.pinEditor(editor)
      }
    }
  }

  stickEditor(
    candidate: EditorInput | undefined = this.activeEditor || undefined,
  ): void {
    this.doStickEditor(candidate, true)
  }

  unstickEditor(
    candidate: EditorInput | undefined = this.activeEditor || undefined,
  ): void {
    this.doStickEditor(candidate, false)
  }

  private doStickEditor(
    candidate: EditorInput | undefined,
    sticky: boolean,
  ): void {
    if (candidate && this.model.isSticky(candidate) !== sticky) {
      const oldIndexOfEditor = this.getIndexOfEditor(candidate)

      // Update model
      const editor = sticky
        ? this.model.stick(candidate)
        : this.model.unstick(candidate)
      if (!editor) {
        return
      }

      // If the index of the editor changed, we need to forward this to
      // title control and also make sure to emit this as an event
      const newIndexOfEditor = this.getIndexOfEditor(editor)
      if (newIndexOfEditor !== oldIndexOfEditor) {
        this.titleControl.moveEditor(
          editor,
          oldIndexOfEditor,
          newIndexOfEditor,
          true,
        )
      }

      // Forward sticky state to title control
      if (sticky) {
        this.titleControl.stickEditor(editor)
      } else {
        this.titleControl.unstickEditor(editor)
      }
    }
  }

  //#endregion

  //#region openEditor()

  async openEditor(
    editor: EditorInput,
    options?: IEditorOptions,
    internalOptions?: IInternalEditorOpenOptions,
    domElement?: HTMLElement,
  ): Promise<IEditorPane | undefined> {
    return this.doOpenEditor(
      editor,
      options,
      {
        // Appply given internal open options
        ...internalOptions,
        // Allow to match on a side-by-side editor when same
        // editor is opened on both sides. In that case we
        // do not want to open a new editor but reuse that one.
        supportSideBySide: SideBySideEditor.BOTH,
      },
      domElement,
    )
  }

  private async doOpenEditor(
    editor: EditorInput,
    options?: IEditorOptions,
    internalOptions?: IInternalEditorOpenOptions,
    domElement?: HTMLElement,
  ): Promise<IEditorPane | undefined> {
    // Guard against invalid editors. Disposed editors
    // should never open because they emit no events
    // e.g. to indicate dirty changes.
    if (!editor || editor.isDisposed()) {
      return
    }

    // Fire the event letting everyone know we are about to open an editor
    this._onWillOpenEditor.fire({ editor, groupId: this.id })

    // Determine options
    const pinned =
      options?.sticky ||
      (!this.groupsView.partOptions.enablePreview && !options?.transient) ||
      editor.isDirty() ||
      (options?.pinned ??
        typeof options?.index ===
          "number") /* unless specified, prefer to pin when opening with index */ ||
      (typeof options?.index === "number" &&
        this.model.isSticky(options.index)) ||
      editor.hasCapability(EditorInputCapabilities.Scratchpad)
    const openEditorOptions: IEditorOpenOptions = {
      index: options ? options.index : undefined,
      pinned,
      sticky:
        options?.sticky ||
        (typeof options?.index === "number" &&
          this.model.isSticky(options.index)),
      transient: !!options?.transient,
      inactiveSelection: internalOptions?.inactiveSelection,
      active: this.count === 0 || !options || !options.inactive,
      supportSideBySide: internalOptions?.supportSideBySide,
    }

    if (
      !openEditorOptions.active &&
      !openEditorOptions.pinned &&
      this.model.activeEditor &&
      !this.model.isPinned(this.model.activeEditor)
    ) {
      // Special case: we are to open an editor inactive and not pinned, but the current active
      // editor is also not pinned, which means it will get replaced with this one. As such,
      // the editor can only be active.
      openEditorOptions.active = true
    }

    let activateGroup = false
    let restoreGroup = false

    if (editor.isGhost()) {
      activateGroup = false
    } else {
      if (options?.activation === EditorActivation.ACTIVATE) {
        // Respect option to force activate an editor group.
        activateGroup = true
      } else if (options?.activation === EditorActivation.RESTORE) {
        // Respect option to force restore an editor group.
        restoreGroup = true
      } else if (options?.activation === EditorActivation.PRESERVE) {
        // Respect option to preserve active editor group.
        activateGroup = false
        restoreGroup = false
      } else if (openEditorOptions.active) {
        // Finally, we only activate/restore an editor which is
        // opening as active editor.
        // If preserveFocus is enabled, we only restore but never
        // activate the group.
        activateGroup = !options || !options.preserveFocus
        restoreGroup = !activateGroup
      }
    }

    // Actually move the editor if a specific index is provided and we figure
    // out that the editor is already opened at a different index. This
    // ensures the right set of events are fired to the outside.
    if (typeof openEditorOptions.index === "number") {
      const indexOfEditor = this.model.indexOf(editor)
      if (indexOfEditor !== -1 && indexOfEditor !== openEditorOptions.index) {
        this.doMoveEditorInsideGroup(editor, openEditorOptions)
      }
    }

    // Update model and make sure to continue to use the editor we get from
    // the model. It is possible that the editor was already opened and we
    // want to ensure that we use the existing instance in that case.
    const { editor: openedEditor, isNew } = this.model.openEditor(
      editor,
      openEditorOptions,
    )

    // Conditionally lock the group
    if (
      isNew && // only if this editor was new for the group
      this.count === 1 && // only when this editor was the first editor in the group
      this.editorPartsView.groups.length > 1 // only allow auto locking if more than 1 group is opened
    ) {
      // only when the editor identifier is configured as such
      if (
        openedEditor.editorId &&
        this.groupsView.partOptions.autoLockGroups?.has(openedEditor.editorId)
      ) {
        this.lock(true)
      }
    }

    // Show editor
    const showEditorResult = this.doShowEditor(
      openedEditor,
      { active: !!openEditorOptions.active, isNew },
      options,
      internalOptions,
      domElement,
    )

    // Finally make sure the group is active or restored as instructed
    if (activateGroup) {
      this.groupsView.activateGroup(this)
    } else if (restoreGroup) {
      this.groupsView.restoreGroup(this)
    }

    return showEditorResult
  }

  private doShowEditor(
    editor: EditorInput,
    context: { active: boolean; isNew: boolean },
    options?: IEditorOptions,
    internalOptions?: IInternalEditorOpenOptions,
    domElement?: HTMLElement,
  ): Promise<IEditorPane | undefined> {
    // Show in editor control if the active editor changed
    let openEditorPromise: Promise<IEditorPane | undefined>
    if (context.active) {
      openEditorPromise = (async () => {
        const result = await this.editorPane.openEditor(
          editor,
          options,
          internalOptions,
          { newInGroup: context.isNew },
          domElement,
        )

        const { pane, changed, cancelled, error } = result

        // Return early if the operation was cancelled by another operation
        if (cancelled) {
          return undefined
        }

        // Editor change event
        if (changed) {
          this._onDidActiveEditorChange.fire({ editor })
        }

        // Indicate error as an event but do not bubble them up
        if (error) {
          this._onDidOpenEditorFail.fire(editor)
        }

        // Without an editor pane, recover by closing the active editor
        // (if the input is still the active one)
        if (!pane && this.activeEditor === editor) {
          this.doCloseEditor(editor, options?.preserveFocus, {
            fromError: true,
          })
        }

        return pane
      })()
    } else {
      openEditorPromise = Promise.resolve(undefined) // inactive: return undefined as result to signal this
    }

    // Show in title control after editor control because some actions depend on it
    // but respect the internal options in case title control updates should skip.
    if (!internalOptions?.skipTitleUpdate) {
      this.titleControl.openEditor(editor, internalOptions)
    }

    return openEditorPromise
  }

  //#endregion

  //#region openEditors()

  async openEditors(
    editors: { editor: EditorInput; options?: IEditorOptions }[],
  ): Promise<IEditorPane | undefined> {
    // Guard against invalid editors. Disposed editors
    // should never open because they emit no events
    // e.g. to indicate dirty changes.
    const editorsToOpen = coalesce(editors).filter(
      ({ editor }) => !editor.isDisposed(),
    )

    // Use the first editor as active editor
    const firstEditor = firstOrDefault(editorsToOpen)
    if (!firstEditor) {
      return
    }

    const openEditorsOptions: IInternalEditorOpenOptions = {
      // Allow to match on a side-by-side editor when same
      // editor is opened on both sides. In that case we
      // do not want to open a new editor but reuse that one.
      supportSideBySide: SideBySideEditor.BOTH,
    }

    await this.doOpenEditor(
      firstEditor.editor,
      firstEditor.options,
      openEditorsOptions,
    )

    // Open the other ones inactive
    const inactiveEditors = editorsToOpen.slice(1)
    const startingIndex = this.getIndexOfEditor(firstEditor.editor) + 1
    const editorResults = await Promises.settled(
      inactiveEditors.map(({ editor, options }, index) => {
        return this.doOpenEditor(
          editor,
          {
            ...options,
            inactive: true,
            pinned: true,
            index: startingIndex + index,
          },
          {
            ...openEditorsOptions,
            // optimization: update the title control later
            // https://github.com/microsoft/vscode/issues/130634
            skipTitleUpdate: true,
          },
        )
      }),
    )

    // Update the title control all at once with all editors
    this.titleControl.openEditors(inactiveEditors.map(({ editor }) => editor))

    // Opening many editors at once can put any editor to be
    // the active one depending on options. As such, we simply
    // return the active editor pane after this operation.
    if (this.editorPane.activeEditorPane != null) {
      const foundActiveEditorPane = editorResults.find(
        (pane) => pane?.getId() === this.editorPane.activeEditorPane?.getId(),
      )
      return foundActiveEditorPane
    }

    return undefined
  }

  //#endregion

  //#region moveEditor()

  moveEditors(
    editors: { editor: EditorInput; options?: IEditorOptions }[],
    target: EditorGroupView,
  ): boolean {
    // Optimization: knowing that we move many editors, we
    // delay the title update to a later point for this group
    // through a method that allows for bulk updates but only
    // when moving to a different group where many editors
    // are more likely to occur.
    const internalOptions: IInternalMoveCopyOptions = {
      skipTitleUpdate: this !== target,
    }

    let moveFailed = false

    const movedEditors = new Set<EditorInput>()
    for (const { editor, options } of editors) {
      if (this.moveEditor(editor, target, options, internalOptions)) {
        movedEditors.add(editor)
      } else {
        moveFailed = true
      }
    }

    // Update the title control all at once with all editors
    // in source and target if the title update was skipped
    if (internalOptions.skipTitleUpdate) {
      target.titleControl.openEditors(Array.from(movedEditors))
      this.titleControl.closeEditors(Array.from(movedEditors))
    }

    return !moveFailed
  }

  moveEditor(
    editor: EditorInput,
    target: EditorGroupView,
    options?: IEditorOptions,
    internalOptions?: IInternalMoveCopyOptions,
  ): boolean {
    // Move within same group
    if (this === target) {
      this.doMoveEditorInsideGroup(editor, options)
      return true
    }

    // Move across groups
    else {
      return this.doMoveOrCopyEditorAcrossGroups(editor, target, options, {
        ...internalOptions,
        keepCopy: false,
      })
    }
  }

  private doMoveEditorInsideGroup(
    candidate: EditorInput,
    options?: IEditorOpenOptions,
  ): void {
    const moveToIndex = options ? options.index : undefined
    if (typeof moveToIndex !== "number") {
      return // do nothing if we move into same group without index
    }

    // Update model and make sure to continue to use the editor we get from
    // the model. It is possible that the editor was already opened and we
    // want to ensure that we use the existing instance in that case.
    const currentIndex = this.model.indexOf(candidate)
    const editor = this.model.getEditorByIndex(currentIndex)
    if (!editor) {
      return
    }

    // Move when index has actually changed
    if (currentIndex !== moveToIndex) {
      const oldStickyCount = this.model.stickyCount

      // Update model
      this.model.moveEditor(editor, moveToIndex)
      this.model.pin(editor)

      // Forward to title control
      this.titleControl.moveEditor(
        editor,
        currentIndex,
        moveToIndex,
        oldStickyCount !== this.model.stickyCount,
      )
      this.titleControl.pinEditor(editor)
    }

    // Support the option to stick the editor even if it is moved.
    // It is important that we call this method after we have moved
    // the editor because the result of moving the editor could have
    // caused a change in sticky state.
    if (options?.sticky) {
      this.stickEditor(editor)
    }
  }

  private doMoveOrCopyEditorAcrossGroups(
    editor: EditorInput,
    target: EditorGroupView,
    openOptions?: IEditorOpenOptions,
    internalOptions?: IInternalMoveCopyOptions,
  ): boolean {
    const keepCopy = internalOptions?.keepCopy

    // Validate that we can move
    if (
      !keepCopy ||
      editor.hasCapability(
        EditorInputCapabilities.Singleton,
      ) /* singleton editors will always move */
    ) {
      const canMoveVeto = editor.canMove(this.id, target.id)
      if (typeof canMoveVeto === "string") {
        this.dialogService.error(
          canMoveVeto,
          localize(
            "moveErrorDetails",
            "Try saving or reverting the editor first and then try again.",
          ),
        )

        return false
      }
    }

    // When moving/copying an editor, try to preserve as much view state as possible
    // by checking for the editor to be a text editor and creating the options accordingly
    // if so
    const options = fillActiveEditorViewState(this, editor, {
      ...openOptions,
      pinned: true, // always pin moved editor
      sticky: openOptions?.sticky ?? (!keepCopy && this.model.isSticky(editor)), // preserve sticky state only if editor is moved or eplicitly wanted (https://github.com/microsoft/vscode/issues/99035)
    })

    // Indicate will move event
    if (!keepCopy) {
      this._onWillMoveEditor.fire({
        groupId: this.id,
        editor,
        target: target.id,
      })
    }

    // A move to another group is an open first...
    target.doOpenEditor(
      keepCopy ? editor.copy() : editor,
      options,
      internalOptions,
    )

    // ...and a close afterwards (unless we copy)
    if (!keepCopy) {
      this.doCloseEditor(
        editor,
        true /* do not focus next one behind if any */,
        { ...internalOptions, context: EditorCloseContext.MOVE },
      )
    }

    return true
  }

  //#endregion

  //#region copyEditor()

  copyEditors(
    editors: { editor: EditorInput; options?: IEditorOptions }[],
    target: EditorGroupView,
  ): void {
    // Optimization: knowing that we move many editors, we
    // delay the title update to a later point for this group
    // through a method that allows for bulk updates but only
    // when moving to a different group where many editors
    // are more likely to occur.
    const internalOptions: IInternalMoveCopyOptions = {
      skipTitleUpdate: this !== target,
    }

    for (const { editor, options } of editors) {
      this.copyEditor(editor, target, options, internalOptions)
    }

    // Update the title control all at once with all editors
    // in target if the title update was skipped
    if (internalOptions.skipTitleUpdate) {
      const copiedEditors = editors.map(({ editor }) => editor)
      target.titleControl.openEditors(copiedEditors)
    }
  }

  copyEditor(
    editor: EditorInput,
    target: EditorGroupView,
    options?: IEditorOptions,
    internalOptions?: IInternalEditorTitleControlOptions,
  ): void {
    // Move within same group because we do not support to show the same editor
    // multiple times in the same group
    if (this === target) {
      this.doMoveEditorInsideGroup(editor, options)
    }

    // Copy across groups
    else {
      this.doMoveOrCopyEditorAcrossGroups(editor, target, options, {
        ...internalOptions,
        keepCopy: true,
      })
    }
  }

  //#endregion

  //#region closeEditor()

  async closeEditor(
    editor: EditorInput | undefined = this.activeEditor || undefined,
    options?: ICloseEditorOptions,
  ): Promise<boolean> {
    if (!editor) return false

    if (options?.closeUnconditionally) {
      // Revert first, then close.
      await editor.revert(this.id)
      this.doCloseEditor(editor, true)
      return true
    }
    return this.doCloseEditorWithConfirmationHandling(editor, options)
  }

  private async doCloseEditorWithConfirmationHandling(
    editor: EditorInput | undefined = this.activeEditor || undefined,
    options?: ICloseEditorOptions,
    internalOptions?: IInternalEditorCloseOptions,
  ): Promise<boolean> {
    if (!editor) {
      return false
    }

    // Check for confirmation and veto
    const veto = await this.handleCloseConfirmation([editor])
    if (veto) {
      return false
    }

    // Do close
    this.doCloseEditor(editor, options?.preserveFocus, internalOptions)

    return true
  }

  private doCloseEditor(
    editor: EditorInput,
    preserveFocus = this.groupsView.activeGroup !== this,
    internalOptions?: IInternalEditorCloseOptions,
  ): void {
    this.editorPane.closeEditorIrrespectiveOfActive(editor)

    // Forward to title control unless skipped via internal options
    if (!internalOptions?.skipTitleUpdate) {
      this.titleControl.beforeCloseEditor(editor)
    }

    // Closing the active editor of the group is a bit more work
    if (this.model.isActive(editor)) {
      this.doCloseActiveEditor(preserveFocus, internalOptions)
    }
    // Closing inactive editor is just a model update
    else {
      this.doCloseInactiveEditor(editor, internalOptions)
    }

    // Forward to title control unless skipped via internal options
    if (!internalOptions?.skipTitleUpdate) {
      this.titleControl.closeEditor(editor)
    }
  }

  private doCloseActiveEditor(
    preserveFocus = this.groupsView.activeGroup !== this,
    internalOptions?: IInternalEditorCloseOptions,
  ): void {
    const editorToClose = this.activeEditor
    const restoreFocus = !preserveFocus && this.shouldRestoreFocus(this.element)

    // Optimization: if we are about to close the last editor in this group and settings
    // are configured to close the group since it will be empty, we first set the last
    // active group as empty before closing the editor. This reduces the amount of editor
    // change events that this operation emits and will reduce flicker. Without this
    // optimization, this group (if active) would first trigger a active editor change
    // event because it became empty, only to then trigger another one when the next
    // group gets active.
    const closeEmptyGroup = this.groupsView.partOptions.closeEmptyGroups
    if (closeEmptyGroup && this.active && this.count === 1) {
      const mostRecentlyActiveGroups = this.groupsView.getGroups(
        GroupsOrder.MOST_RECENTLY_ACTIVE,
      )
      const nextActiveGroup = mostRecentlyActiveGroups[1] // [0] will be the current one, so take [1]
      if (nextActiveGroup) {
        if (restoreFocus) {
          nextActiveGroup.focus()
        } else {
          this.groupsView.activateGroup(nextActiveGroup, true)
        }
      }
    }

    // Update model if necessary.
    if (editorToClose) {
      this.model.closeEditor(editorToClose, internalOptions?.context)
    }

    const nextActiveEditor = this.model.activeEditor
    if (nextActiveEditor) {
      let activation: EditorActivation | undefined = undefined
      if (preserveFocus && this.groupsView.activeGroup !== this) {
        // If we are opening the next editor in an inactive group
        // without focussing it, ensure we preserve the editor
        // group sizes in case that group is minimized.
        // https://github.com/microsoft/vscode/issues/117686
        activation = EditorActivation.PRESERVE
      }

      const options: IEditorOptions = {
        preserveFocus,
        activation,
        // When closing an editor due to an error we can end up in a loop where we continue closing
        // editors that fail to open (e.g. when the file no longer exists). We do not want to show
        // repeated errors in this case to the user. As such, if we open the next editor and we are
        // in a scope of a previous editor failing, we silence the input errors until the editor is
        // opened by setting ignoreError: true.
        ignoreError: internalOptions?.fromError,
      }

      const internalEditorOpenOptions: IInternalEditorOpenOptions = {
        // When closing an editor, we reveal the next one in the group.
        // However, this can be a result of moving an editor to another
        // window so we explicitly disable window reordering in this case.
        preserveWindowOrder: true,
      }

      this.doOpenEditor(nextActiveEditor, options, internalEditorOpenOptions)
    }

    // Otherwise we are empty, so clear from editor control and send event
    else {
      // Forward to editor pane
      if (editorToClose) {
        this.editorPane.closeEditor(editorToClose)
      }

      // Restore focus to group container as needed unless group gets closed
      if (restoreFocus && !closeEmptyGroup) {
        this.focus()
      }

      // Events
      this._onDidActiveEditorChange.fire({ editor: undefined })

      // Remove empty group if we should
      if (closeEmptyGroup) {
        this.groupsView.removeGroup(this, preserveFocus)
      }
    }
  }

  private shouldRestoreFocus(target: Element): boolean {
    const activeElement = getActiveElement()
    if (activeElement === target.ownerDocument.body) {
      return true // always restore focus if nothing is focused currently
    }

    // otherwise check for the active element being an ancestor of the target
    return isAncestor(activeElement, target)
  }

  private doCloseInactiveEditor(
    editor: EditorInput,
    internalOptions?: IInternalEditorCloseOptions,
  ): void {
    // Update model
    this.model.closeEditor(editor, internalOptions?.context)
  }

  private async handleCloseConfirmation(
    editors: EditorInput[],
  ): Promise<boolean /* veto */> {
    if (!editors.length) {
      return false // no veto
    }

    const editor = editors.shift()!

    // To prevent multiple confirmation dialogs from showing up one after the other
    // we check if a pending confirmation is currently showing and if so, join that
    let handleCloseConfirmationPromise =
      this.mapEditorToPendingConfirmation.get(editor)
    if (!handleCloseConfirmationPromise) {
      handleCloseConfirmationPromise = this.doHandleCloseConfirmation(editor)
      this.mapEditorToPendingConfirmation.set(
        editor,
        handleCloseConfirmationPromise,
      )
    }

    let veto: boolean
    try {
      veto = await handleCloseConfirmationPromise
    } finally {
      this.mapEditorToPendingConfirmation.delete(editor)
    }

    // Return for the first veto we got
    if (veto) {
      return veto
    }

    // Otherwise continue with the remainders
    return this.handleCloseConfirmation(editors)
  }

  private async doHandleCloseConfirmation(
    editor: EditorInput,
    options?: { skipAutoSave: boolean },
  ): Promise<boolean /* veto */> {
    if (!this.shouldConfirmClose(editor)) {
      return false // no veto
    }

    if (
      editor instanceof SideBySideEditorInput &&
      this.model.contains(editor.primary)
    ) {
      return false // primary-side of editor is still opened somewhere else
    }

    // Note: we explicitly decide to ask for confirm if closing a normal editor even
    // if it is opened in a side-by-side editor in the group. This decision is made
    // because it may be less obvious that one side of a side by side editor is dirty
    // and can still be changed.
    // The only exception is when the same editor is opened on both sides of a side
    // by side editor (https://github.com/microsoft/vscode/issues/138442)

    if (
      this.editorPartsView.groups.some((groupView) => {
        if (groupView === this) {
          return false // skip (we already handled our group above)
        }

        const otherGroup = groupView
        if (
          otherGroup.contains(editor, {
            supportSideBySide: SideBySideEditor.BOTH,
          })
        ) {
          return true // exact editor still opened (either single, or split-in-group)
        }

        if (
          editor instanceof SideBySideEditorInput &&
          otherGroup.contains(editor.primary)
        ) {
          return true // primary side of side by side editor still opened
        }

        return false
      })
    ) {
      return false // editor is still editable somewhere else
    }

    // In some cases trigger save before opening the dialog depending
    // on auto-save configuration.
    // However, make sure to respect `skipAutoSave` option in case the automated
    // save fails which would result in the editor never closing.
    // Also, we only do this if no custom confirmation handling is implemented.
    let confirmation = ConfirmResult.CANCEL
    let saveReason = SaveReason.EXPLICIT
    let autoSave = false
    if (
      !editor.hasCapability(EditorInputCapabilities.Untitled) &&
      !options?.skipAutoSave &&
      !editor.closeHandler
    ) {
      // Auto-save on focus change: save, because a dialog would steal focus
      // (see https://github.com/microsoft/vscode/issues/108752)
      if (
        this.filesConfigurationService.getAutoSaveMode(editor).mode ===
        AutoSaveMode.ON_FOCUS_CHANGE
      ) {
        autoSave = true
        confirmation = ConfirmResult.SAVE
        saveReason = SaveReason.FOCUS_CHANGE
      }

      // Auto-save on window change: save, because on Windows and Linux, a
      // native dialog triggers the window focus change
      // (see https://github.com/microsoft/vscode/issues/134250)
      else if (
        isNative &&
        (isWindows || isLinux) &&
        this.filesConfigurationService.getAutoSaveMode(editor).mode ===
          AutoSaveMode.ON_WINDOW_CHANGE
      ) {
        autoSave = true
        confirmation = ConfirmResult.SAVE
        saveReason = SaveReason.WINDOW_CHANGE
      }
    }

    // No auto-save on focus change or custom confirmation handler: ask user
    if (!autoSave) {
      // Switch to editor that we want to handle for confirmation unless showing already
      if (!this.activeEditor || !this.activeEditor.matches(editor)) {
        await this.doOpenEditor(editor)
      }

      // Ensure our window has focus since we are about to show a dialog
      await this.hostService.focus(getWindow(this.element))

      // Let editor handle confirmation if implemented
      if (typeof editor.closeHandler?.confirm === "function") {
        confirmation = await editor.closeHandler.confirm([
          { editor, groupId: this.id },
        ])
      }

      // Show a file specific confirmation
      else {
        let name: string
        if (editor instanceof SideBySideEditorInput) {
          name = editor.primary.getName() // prefer shorter names by using primary's name in this case
        } else {
          name = editor.getName()
        }

        confirmation = await this.fileDialogService.showSaveConfirm([name])
      }
    }

    // It could be that the editor's choice of confirmation has changed
    // given the check for confirmation is long running, so we check
    // again to see if anything needs to happen before closing for good.
    // This can happen for example if `autoSave: onFocusChange` is configured
    // so that the save happens when the dialog opens.
    // However, we only do this unless a custom confirm handler is installed
    // that may not be fit to be asked a second time right after.
    if (!editor.closeHandler && !this.shouldConfirmClose(editor)) {
      return confirmation === ConfirmResult.CANCEL ? true : false
    }

    // Otherwise, handle accordingly
    switch (confirmation) {
      case ConfirmResult.SAVE: {
        const result = await editor.save(this.id, { reason: saveReason })
        if (!result && autoSave) {
          // Save failed and we need to signal this back to the user, so
          // we handle the dirty editor again but this time ensuring to
          // show the confirm dialog
          // (see https://github.com/microsoft/vscode/issues/108752)
          return this.doHandleCloseConfirmation(editor, { skipAutoSave: true })
        }

        return editor.isDirty() // veto if still dirty
      }
      case ConfirmResult.DONT_SAVE:
        try {
          // first try a normal revert where the contents of the editor are restored
          await editor.revert(this.id)

          return editor.isDirty() // veto if still dirty
        } catch (error) {
          this.logService.error(error)

          // if that fails, since we are about to close the editor, we accept that
          // the editor cannot be reverted and instead do a soft revert that just
          // enables us to close the editor. With this, a user can always close a
          // dirty editor even when reverting fails.

          await editor.revert(this.id, { soft: true })

          return editor.isDirty() // veto if still dirty
        }
      case ConfirmResult.CANCEL:
        return true // veto
    }
  }

  private shouldConfirmClose(editor: EditorInput): boolean {
    if (editor.closeHandler) {
      return editor.closeHandler.showConfirm() // custom handling of confirmation on close
    }

    return editor.isDirty() && !editor.isSaving() // editor must be dirty and not saving
  }

  //#endregion

  //#region closeEditors()

  async closeEditors(
    args: EditorInput[] | ICloseEditorsFilter,
    options?: ICloseEditorOptions,
  ): Promise<boolean> {
    if (this.isEmpty) {
      return true
    }

    const editors = this.doGetEditorsToClose(args)

    // Check for confirmation and veto
    const veto = await this.handleCloseConfirmation(editors.slice(0))
    if (veto) {
      return false
    }

    // Do close
    this.doCloseEditors(editors, options)

    return true
  }

  private doGetEditorsToClose(
    args: EditorInput[] | ICloseEditorsFilter,
  ): EditorInput[] {
    if (Array.isArray(args)) {
      return args
    }

    const filter = args
    const hasDirection = typeof filter.direction === "number"

    let editorsToClose = this.model.getEditors(
      hasDirection
        ? EditorsOrder.SEQUENTIAL
        : EditorsOrder.MOST_RECENTLY_ACTIVE,
      filter,
    ) // in MRU order only if direction is not specified

    // Filter: saved or saving only
    if (filter.savedOnly) {
      editorsToClose = editorsToClose.filter(
        (editor) => !editor.isDirty() || editor.isSaving(),
      )
    }

    // Filter: direction (left / right)
    else if (hasDirection && filter.except) {
      editorsToClose =
        filter.direction === CloseDirection.LEFT
          ? editorsToClose.slice(
              0,
              this.model.indexOf(filter.except, editorsToClose),
            )
          : editorsToClose.slice(
              this.model.indexOf(filter.except, editorsToClose) + 1,
            )
    }

    // Filter: except
    else if (filter.except) {
      editorsToClose = editorsToClose.filter(
        (editor) => filter.except && !editor.matches(filter.except),
      )
    }

    return editorsToClose
  }

  private doCloseEditors(
    editors: EditorInput[],
    options?: ICloseEditorOptions,
  ): void {
    // Close all inactive editors first
    let closeActiveEditor = false
    for (const editor of editors) {
      if (options?.closeUnconditionally) {
        // Revert first, then close.
        editor.revert(this.id).then(() => {
          this.doCloseEditor(editor, true)
        })
        continue
      }
      if (!this.isActive(editor)) {
        this.doCloseInactiveEditor(editor)
      } else {
        closeActiveEditor = true
      }
    }

    // Close active editor last if contained in editors list to close
    if (closeActiveEditor) {
      this.doCloseActiveEditor(options?.preserveFocus)
    }

    // for (const editor of editors) {
    // 	this.editorPane.closeEditorIrrespectiveOfActive(editor)
    // }

    // Forward to title control
    if (editors.length) {
      this.titleControl.closeEditors(editors)
    }
  }

  //#endregion

  //#region closeAllEditors()

  async closeAllEditors(options?: ICloseAllEditorsOptions): Promise<boolean> {
    if (this.isEmpty) {
      // If the group is empty and the request is to close all editors, we still close
      // the editor group is the related setting to close empty groups is enabled for
      // a convenient way of removing empty editor groups for the user.
      if (this.groupsView.partOptions.closeEmptyGroups) {
        this.groupsView.removeGroup(this)
      }

      return true
    }

    // Apply the `excludeConfirming` filter if present
    let editors = this.model.getEditors(
      EditorsOrder.MOST_RECENTLY_ACTIVE,
      options,
    )
    if (options?.forceClose) {
      // Revert first, then close.
      for (const editor of editors) {
        await editor.revert(this.id)
      }
    } else {
      if (options?.excludeConfirming) {
        editors = editors.filter((editor) => !this.shouldConfirmClose(editor))
      }

      // Check for confirmation and veto
      const veto = await this.handleCloseConfirmation(editors)
      if (veto) {
        return false
      }
    }

    // Do close
    this.doCloseAllEditors(options)

    return true
  }

  private doCloseAllEditors(options?: ICloseAllEditorsOptions): void {
    let editors = this.model.getEditors(EditorsOrder.SEQUENTIAL, options)
    if (options?.excludeConfirming) {
      editors = editors.filter((editor) => !this.shouldConfirmClose(editor))
    }

    // Close all inactive editors first
    const editorsToClose: EditorInput[] = []
    for (const editor of editors) {
      if (!this.isActive(editor)) {
        this.doCloseInactiveEditor(editor)
      }

      editorsToClose.push(editor)
    }

    // Close active editor last (unless we skip it, e.g. because it is sticky)
    if (this.activeEditor && editorsToClose.includes(this.activeEditor)) {
      this.doCloseActiveEditor()
    }

    // Forward to title control
    if (editorsToClose.length) {
      this.titleControl.closeEditors(editorsToClose)
    }
  }

  //#endregion

  //#region replaceEditors()

  async replaceEditors(editors: EditorReplacement[]): Promise<void> {
    // Extract active vs. inactive replacements
    let activeReplacement: EditorReplacement | undefined
    const inactiveReplacements: EditorReplacement[] = []
    for (let { editor, replacement, forceReplaceDirty, options } of editors) {
      const index = this.getIndexOfEditor(editor)
      if (index >= 0) {
        const isActiveEditor = this.isActive(editor)

        // make sure we respect the index of the editor to replace
        if (options) {
          options.index = index
        } else {
          options = { index }
        }

        options.inactive = !isActiveEditor
        options.pinned = options.pinned ?? true // unless specified, prefer to pin upon replace

        const editorToReplace = {
          editor,
          replacement,
          forceReplaceDirty,
          options,
        }
        if (isActiveEditor) {
          activeReplacement = editorToReplace
        } else {
          inactiveReplacements.push(editorToReplace)
        }
      }
    }

    // We do not have the notion of inactive editors, so do not handle them.
    if (activeReplacement) {
      // Open replacement as active editor
      // Make sure we set active editor to null first.
      this.model.clearActiveEditor()

      await this._haystackService.createFileEditor(
        activeReplacement.replacement.resource,
        {
          replaceExistingEditorInput: {
            input: activeReplacement.editor,
            groupId: this.id,
          },
        },
        activeReplacement.options,
      )
      if (activeReplacement.forceReplaceDirty) {
        this.doCloseEditor(activeReplacement.editor, true, {
          context: EditorCloseContext.REPLACE,
        })
      } else {
        await this.doCloseEditorWithConfirmationHandling(
          activeReplacement.editor,
          { preserveFocus: true },
          { context: EditorCloseContext.REPLACE },
        )
      }
    }
  }

  //#endregion

  //#region Locking

  get isLocked(): boolean {
    return this.model.isLocked
  }

  lock(locked: boolean): void {
    this.model.lock(locked)
  }

  //#endregion

  //#region Editor Actions

  createEditorActions(disposables: DisposableStore): IActiveEditorActions {
    const primary: IAction[] = []
    const secondary: IAction[] = []

    let onDidChange

    // Editor actions require the editor control to be there, so we retrieve it via service
    const activeEditorPane = this.activeEditorPane
    if (activeEditorPane instanceof EditorPane) {
      const editorScopedContextKeyService =
        activeEditorPane.scopedContextKeyService ?? this.scopedContextKeyService
      const editorTitleMenu = disposables.add(
        this.menuService.createMenu(
          MenuId.EditorTitle,
          editorScopedContextKeyService,
          { emitEventsForSubmenuChanges: true, eventDebounceDelay: 0 },
        ),
      )
      onDidChange = editorTitleMenu.onDidChange

      const shouldInlineGroup = (action: SubmenuAction, group: string) =>
        group === "navigation" && action.actions.length <= 1

      createAndFillInActionBarActions(
        editorTitleMenu,
        { arg: this.resourceContext.get(), shouldForwardArgs: true },
        { primary, secondary },
        "navigation",
        shouldInlineGroup,
      )
    } else {
      // If there is no active pane in the group (it's the last group and it's empty)
      // Trigger the change event when the active editor changes
      const _onDidChange = disposables.add(new Emitter<void>())
      onDidChange = _onDidChange.event
      disposables.add(this.onDidActiveEditorChange(() => _onDidChange.fire()))
    }

    return { actions: { primary, secondary }, onDidChange }
  }

  //#endregion

  //#region Themable

  override updateStyles(): void {
    const isEmpty = this.isEmpty

    // Container
    if (isEmpty) {
      this.element.style.backgroundColor =
        this.getColor(EDITOR_GROUP_EMPTY_BACKGROUND) || ""
    } else {
      this.element.style.backgroundColor = ""
    }

    // Title control
    const borderColor =
      this.getColor(EDITOR_GROUP_HEADER_BORDER) || this.getColor(contrastBorder)
    if (!isEmpty && borderColor) {
      this.titleContainer.classList.add("title-border-bottom")
      this.titleContainer.style.setProperty(
        "--title-border-bottom-color",
        borderColor,
      )
    } else {
      this.titleContainer.classList.remove("title-border-bottom")
      this.titleContainer.style.removeProperty("--title-border-bottom-color")
    }

    const { showTabs } = this.groupsView.partOptions
    this.titleContainer.style.backgroundColor =
      this.getColor(
        showTabs === "multiple"
          ? EDITOR_GROUP_HEADER_TABS_BACKGROUND
          : EDITOR_GROUP_HEADER_NO_TABS_BACKGROUND,
      ) || ""

    // Editor container
    this.editorContainer.style.backgroundColor =
      this.getColor(editorBackground) || ""
  }

  //#endregion

  //#region ISerializableView

  readonly element: HTMLElement = document.createElement("div")

  get minimumWidth(): number {
    return this.editorPane.minimumWidth
  }
  get minimumHeight(): number {
    return this.editorPane.minimumHeight
  }
  get maximumWidth(): number {
    return this.editorPane.maximumWidth
  }
  get maximumHeight(): number {
    return this.editorPane.maximumHeight
  }

  get proportionalLayout(): boolean {
    if (!this.lastLayout) {
      return true
    }

    return !(
      this.lastLayout.width === this.minimumWidth ||
      this.lastLayout.height === this.minimumHeight
    )
  }

  private _onDidChange = this._register(
    new Relay<{ width: number; height: number } | undefined>(),
  )
  readonly onDidChange = this._onDidChange.event

  layout(width: number, height: number, top: number, left: number): void {
    this.lastLayout = { width, height, top, left }
    this.element.classList.toggle("max-height-478px", height <= 478)

    // Layout the title control first to receive the size it occupies
    const titleControlSize = this.titleControl.layout({
      container: new Dimension(width, height),
      available: new Dimension(width, height - this.editorPane.minimumHeight),
    })

    // Update progress bar location
    this.progressBar.getContainer().style.top = `${Math.max(
      this.titleHeight.offset - 2,
      0,
    )}px`

    // Pass the container width and remaining height to the editor layout
    const editorHeight = Math.max(0, height - titleControlSize.height)
    this.editorContainer.style.height = `${editorHeight}px`
    this.editorPane.layout({
      width,
      height: editorHeight,
      top: top + titleControlSize.height,
      left,
    })
  }

  relayout(): void {
    if (this.lastLayout) {
      const { width, height, top, left } = this.lastLayout
      this.layout(width, height, top, left)
    }
  }

  setBoundarySashes(sashes: IBoundarySashes): void {
    this.editorPane.setBoundarySashes(sashes)
  }

  shouldSerialize(): boolean {
    let hasOnlyGhostEditors = true
    for (const editor of this.editorPane.getEditorPanes()) {
      if (editor.input == null) continue
      if (!this._haystackService.getIsGhostEditor(editor.input)) {
        hasOnlyGhostEditors = false
        break
      }
    }
    return !this.isEmpty && !hasOnlyGhostEditors
  }

  toJSON(): ISerializedEditorGroupModel {
    for (const editor of this.model.getEditors(EditorsOrder.SEQUENTIAL)) {
      const isGhost = this._haystackService.getIsGhostEditor(editor)
      editor.setIsGhost(isGhost)

      const transformInfo =
        this._haystackService.getTransformInfoForEditor(editor)
      if (transformInfo == null) continue
      const { xPosition, yPosition, width, height } = transformInfo
      editor.setPosition(xPosition, yPosition)
      editor.setSize(width, height)
    }
    return this.model.serialize()
  }

  //#endregion

  override dispose(): void {
    this._disposed = true

    this._onWillDispose.fire()

    super.dispose()
  }
}

export interface EditorReplacement extends IEditorReplacement {
  readonly editor: EditorInput
  readonly replacement: EditorInput
  readonly options?: IEditorOptions
}
